<!DOCTYPE html>
<html lang="zh-Hans">
<meta name="theme-color" content="#FFFFFF">


<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
  <meta name="theme-color" content="#202020"/>
  <meta http-equiv="x-ua-compatible" content="ie=edge">
  <link rel="shortcut icon" href="/images/favicon.ico" type="image/x-icon" />
  <link rel="icon" sizes="any" mask href="/images/favicon.ico" /> 
  
  

  <!-- viewjs support -->
  
    <!-- for theme: default is false -->
    <!-- for page: default is true -->
    <link  href="./css/viewer.min.css" rel="stylesheet" >
    <script src="./js/viewer.min.js" type="text/javascript" charset="utf-8"></script>
  
  
  
    <meta name="keywords" content="tech,SQL," />
  

  
    <meta name="description" content="分享 Java 技术，做有趣的程序员" />
  
  
  <link rel="icon" type="image/x-icon" href="/logo.png">
  <title>MySQL 中的锁 [ H E R O ]</title>
  
    <!-- stylesheets list from config.yml -->
    
      <link rel="stylesheet" href="//cdn.bootcss.com/pure/1.0.0/pure-min.css">
    
      <link rel="stylesheet" href="/css/xoxo.css">
    
  
<meta name="generator" content="Hexo 5.4.0"></head>


<body>
  <div class="nav-container">
    <nav id="menu" class="home-menu pure-menu pure-menu-horizontal">
  <a class="pure-menu-heading" href="/">
    <!-- <img class="avatar" src="/images/logo.png"> -->
    <span class="title">H E R O</span>
  </a>

  <ul class="pure-menu-list clearfix">
      
          
            <li class="pure-menu-item"><a href="/" class="pure-menu-link">首页</a></li>
          
      
          
            <li class="pure-menu-item"><a href="/archives/" class="pure-menu-link">归档</a></li>
          
      
          
            <li class="pure-menu-item"><a href="/tags/" class="pure-menu-link">标签</a></li>
          
      
          
            <li class="pure-menu-item"><a href="/about/" class="pure-menu-link">关于</a></li>
          
      
          
            <li class="pure-menu-item"><a href="/atom.xml" class="pure-menu-link">订阅</a></li>
          
      
  </ul>
   
</nav>
  </div>

  <div class="container" id="content-outer">
    <div class="inner" id="content-inner">
      <div class="post-container">
  <article class="post" id="post">
    <header class="post-header text-center">
      <h1 class="title">
        MySQL 中的锁
      </h1>
      <span>
        
        <time class="time" datetime="2018-09-20T12:15:58.000Z">
          2018-09-20
        </time>
        
      </span>
      <span class="slash">/</span>
      <span class="post-meta">
        <span class="post-tags">
          <ul class="article-tag-list" itemprop="keywords"><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/SQL/" rel="tag">SQL</a></li><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/tech/" rel="tag">tech</a></li></ul>
        </span>
      </span>
      
     
    </header>

    <div class="post-content">
      <p>什么是事务？什么是隔离级别？<br>数据库中使用了多少种锁？这些锁之间有什么关系？<br>数据库如何在并发度和各种并发问题中平衡？<br><span id="more"></span></p>
<h1 id="事务与隔离级别"><a href="#事务与隔离级别" class="headerlink" title="事务与隔离级别"></a>事务与隔离级别</h1><h2 id="事务四要素"><a href="#事务四要素" class="headerlink" title="事务四要素"></a>事务四要素</h2><ul>
<li>原子性（Atomicity）：要么全部完成，要么全部不完成；</li>
<li>一致性（Consistency）：一个事务单元需要提交之后才会被其他事务可见；</li>
<li>隔离性（Isolation）：并发事务之间不会互相影响，设立了不同程度的隔离级别，通过适度的破坏一致性，得以提高性能；</li>
<li>持久性（Durability）：事务提交后即持久化到磁盘不会丢失。</li>
</ul>
<h2 id="并发问题名词解释"><a href="#并发问题名词解释" class="headerlink" title="并发问题名词解释"></a>并发问题名词解释</h2><p>以下表为例子<br><img src="https://image-hosting-1251780645.cos.ap-beijing.myqcloud.com/fromSina/006tNbRwgy1fvfvewig2kj30ep04f0t0.jpg" alt="table"></p>
<h3 id="脏读-dirty-read"><a href="#脏读-dirty-read" class="headerlink" title="脏读 dirty read"></a>脏读 dirty read</h3><p>没有提交的事务被其他事务读取到了，这叫做<strong>脏读</strong>。<br><img src="https://image-hosting-1251780645.cos.ap-beijing.myqcloud.com/fromSina/006tNbRwgy1fvfvg5ajgyj30r60gv0u7.jpg" alt="dirty read"><br>事务开始之前两个人各有1000元<br>事务一执行了转账操作，事务二统计两个人一共有多少钱。事务2的到的结果明显是错误的。</p>
<h3 id="不可重复读-unrepeatable-read"><a href="#不可重复读-unrepeatable-read" class="headerlink" title="不可重复读 unrepeatable read"></a>不可重复读 unrepeatable read</h3><p>同一个事务对同一条记录读取两遍，两次读出来的结果不一样，称为<strong>不可重复读</strong><br><img src="https://image-hosting-1251780645.cos.ap-beijing.myqcloud.com/fromSina/006tNbRwgy1fvfvib1im1j30r60gft9y.jpg" alt="unrepeatabel read"><br>事务2中两次执行同一条SQL得到的结果却不同。不可重复读与脏读的区别在于，不可重复读读到的是其他事务已经提交的修改，而脏读是读到了其他事务还未提交的修改。</p>
<h3 id="幻读-phantom-read"><a href="#幻读-phantom-read" class="headerlink" title="幻读 phantom read"></a>幻读 phantom read</h3><p>同样的条件，第一次和第二次读出来的记录数不一样，称为<strong>幻读</strong><br><img src="https://image-hosting-1251780645.cos.ap-beijing.myqcloud.com/fromSina/006tNbRwgy1fvg0m43z6oj30r60fwwfo.jpg" alt="phantom read"><br>同样的条件，第一次和第二次读出来的记录集合不一样。不可重复读是因为其他事务进行了 UPDATE 操作，幻读是因为其他事务进行了 INSERT 或者 DELETE 操作。<br>事务 2 的两次查询，第一次查出 2 条记录，第二次却查出 3 条记录，多出来的这条记录，正如 phantom（幽灵，幻影，错觉） 的意思，就像幽灵一样。</p>
<h3 id="丢失更新-lost-update"><a href="#丢失更新-lost-update" class="headerlink" title="丢失更新 lost update"></a>丢失更新 lost update</h3><p>两个事务都是写操作，某种情况下有些修改被提交后又被覆盖了，称为<strong>丢失更新</strong><br><img src="https://image-hosting-1251780645.cos.ap-beijing.myqcloud.com/fromSina/006tNbRwgy1fvg0h3ckcrj30r60buabh.jpg" alt="lost update"><br>上图中事务2的提交覆盖了事务1的提交。<br><img src="https://image-hosting-1251780645.cos.ap-beijing.myqcloud.com/fromSina/006tNbRwgy1fvg0k0v4vbj30r60aqq45.jpg" alt="rollback"><br>上图中事务1的回滚直接忽视了事务2的 UPDATE 操作</p>
<h2 id="隔离级别"><a href="#隔离级别" class="headerlink" title="隔离级别"></a>隔离级别</h2><p>为了有效的保证数据一致性和数据库并发性能，便有了四种不同的数据库隔离级别</p>
<ul>
<li>读未提交（Read Uncommitted）：可以读取未提交的记录，会出现脏读，幻读，不可重复读，所有并发问题都可能遇到；</li>
<li>读已提交（Read Committed）：事务中只能看到已提交的修改，不会出现脏读现象，但是会出现幻读，不可重复读；（大多数数据库的默认隔离级别都是 RC，但是 MySQL InnoDb 默认是 RR）</li>
<li>可重复读（Repeatable Read）：MySQL InnoDb 默认的隔离级别，解决了不可重复读问题，但是任然存在幻读问题；（MySQL 的实现有差异，后面介绍）</li>
<li>序列化（Serializable）：最高隔离级别，啥并发问题都没有。</li>
</ul>
<p>针对这四种隔离级别，应该根据具体的业务来取舍，如果某个系统的业务里根本就不会出现重复读的场景，完全可以将数据库的隔离级别设置为 RC，这样可以最大程度的提高数据库的并发性。不同的隔离级别和可能发生的并发现象如下表：<br><img src="https://image-hosting-1251780645.cos.ap-beijing.myqcloud.com/fromSina/006tNbRwgy1fvg0rg5l09j30oh05h0tz.jpg" alt=""></p>
<blockquote>
<p>RR级别下会出现提交覆盖吗？答案文末揭晓。</p>
</blockquote>
<h2 id="隔离级别的实现"><a href="#隔离级别的实现" class="headerlink" title="隔离级别的实现"></a>隔离级别的实现</h2><p>上面所说的都是事务和隔离级别的概念，是 SQL 标准中通用的概念，不同的数据库产品有不同的实现。</p>
<h3 id="传统的隔离级别"><a href="#传统的隔离级别" class="headerlink" title="传统的隔离级别"></a>传统的隔离级别</h3><p>传统的隔离级别是基于锁实现的，这种方式叫做 基于锁的并发控制（Lock-Based Concurrent Control，简写 LBCC）<br>通过对读写操作加不同的锁，以及对释放锁的时机进行不同的控制，就可以实现四种隔离级别。传统的锁有两种：读操作通常加共享锁（Share locks，S锁，又叫读锁），写操作加排它锁（Exclusive locks，X锁，又叫写锁）；加了共享锁的记录，其他事务也可以读，但不能写；加了排它锁的记录，其他事务既不能读，也不能写。另外，对于锁的粒度，又分为行锁和表锁，行锁只锁某行记录，对其他行的操作不受影响，表锁会锁住整张表，所有对这个表的操作都受影响。</p>
<p>归纳起来，四种隔离级别的加锁策略如下：</p>
<ul>
<li>读未提交（Read Uncommitted）：事务读不阻塞其他事务读和写，事务写阻塞其他事务写但不阻塞读；通过对写操作加 “持续X锁”，对读操作不加锁 实现；</li>
<li>读已提交（Read Committed）：事务读不会阻塞其他事务读和写，事务写会阻塞其他事务读和写；通过对写操作加 “持续X锁”，对读操作加 “临时S锁” 实现；不会出现脏读；</li>
<li>可重复读（Repeatable Read）：事务读会阻塞其他事务事务写但不阻塞读，事务写会阻塞其他事务读和写；通过对写操作加 “持续X锁”，对读操作加 “持续S锁” 实现；</li>
<li>序列化（Serializable）：为了解决幻读问题，行级锁做不到，需使用表级锁。</li>
</ul>
<p>通过对<strong>锁的类型（读锁还是写锁），锁的粒度（行锁还是表锁），持有锁的时间（临时锁还是持续锁）</strong>合理的进行组合，就可以实现四种不同的隔离级别。这四种不同的加锁策略实际上又称为 <strong>封锁协议（Locking Protocol）</strong>，所谓协议，就是说不论加锁还是释放锁都得按照特定的规则来。读未提交 的加锁策略又称为 <strong>一级封锁协议</strong>，后面的分别是二级，三级，序列化 的加锁策略又称为 <strong>四级封锁协议</strong>。</p>
<p>其中三级封锁协议在事务的过程中为写操作加持续 X 锁，为读操作加持续 S 锁，并且在事务结束时才对锁进行释放，像这种加锁和解锁明确的分成两个阶段我们把它称作 <strong>两段锁协议（2-phase locking，简称 2PL）</strong>。在两段锁协议中规定，加锁阶段只允许加锁，不允许解锁；而解锁阶段只允许解锁，不允许加锁。这种方式虽然无法避免死锁，但是两段锁协议可以保证事务的并发调度是串行化的（关于串行化是一个非常重要的概念，尤其是在数据恢复和备份的时候）。在两段锁协议中，还有一种特殊的形式，叫 <strong>一次封锁</strong>，意思是指在事务开始的时候，将事务可能遇到的数据全部一次锁住，再在事务结束时全部一次释放，这种方式可以有效的避免死锁发生。</p>
<h3 id="MySQL的隔离级别"><a href="#MySQL的隔离级别" class="headerlink" title="MySQL的隔离级别"></a>MySQL的隔离级别</h3><p>虽然数据库的四种隔离级别通过 LBCC 技术都可以实现，但是它最大的问题是它只实现了并发的读读，对于并发的读写还是冲突的，写时不能读，读时不能写，当读写操作都很频繁时，数据库的并发性将大大降低，针对这种场景，MVCC 技术应运而生。MVCC 的全称叫做 <strong>Multi-Version Concurrent Control（多版本并发控制）</strong>。<br>InnoDb 会为每一行记录增加几个隐含的“辅助字段”，（实际上是 3 个字段：一个隐式的 ID 字段，一个事务 ID，还有一个回滚指针），事务在写一条记录时会将其拷贝一份生成这条记录的一个原始拷贝，写操作同样还是会对原记录加锁，但是读操作会读取未加锁的新记录，这就保证了读写并行。要注意的是，生成的新版本其实就是 undo log，它也是实现事务回滚的关键技术。<br><img src="https://image-hosting-1251780645.cos.ap-beijing.myqcloud.com/fromSina/006tNbRwgy1fvg3oyq9u1j30fm080dgk.jpg" alt=""></p>
<p>在 read uncommit 隔离级别下，每次都是读取最新版本的数据行，所以不能用 MVCC 的多版本，而 serializable 隔离级别每次读取操作都会为记录加上读锁，也和 MVCC 不兼容，所以只有 RC 和 RR 这两个隔离级别才有 MVCC。</p>
<p>RR 和 RC 隔离级别都实现了 MVCC 来满足读写并行，但是读的实现方式是不一样的：RC 总是读取记录的最新版本，如果该记录被锁住，则读取该记录最新的一次快照，而 RR 是读取该记录事务开始时的那个版本。虽然这两种读取方式不一样，但是它们读取的都是快照数据，并不会被写操作阻塞，所以这种读操作称为 <strong>快照读（Snapshot Read）</strong>，有时候也叫做 <strong>非阻塞读（Nonlocking Read）</strong></p>
<p>除了 快照读 ，MySQL 还提供了另一种读取方式：当前读（Current Read），有时候又叫做 加锁读（Locking Read） 或者 阻塞读（Blocking Read），这种读操作读的不再是数据的快照版本，而是数据的最新版本，并会对数据加锁，根据加锁的不同，又分成两类：</p>
<ul>
<li>SELECT … LOCK IN SHARE MODE：加 S 锁</li>
<li>SELECT … FOR UPDATE：加 X 锁</li>
<li>INSERT / UPDATE / DELETE：加 X 锁</li>
</ul>
<p>当前读在 RR 和 RC 两种隔离级别下的实现也是不一样的：RC 只加记录锁，RR 除了加记录锁，还会加间隙锁，用于解决幻读问题。</p>
<p>不同隔离级别下InnoDB的读操作<br><img src="https://image-hosting-1251780645.cos.ap-beijing.myqcloud.com/fromSina/006tNbRwgy1fvg3stivqyj30ow06rq4o.jpg" alt=""></p>
<h2 id="答案"><a href="#答案" class="headerlink" title="答案"></a>答案</h2><p>MySQL 的实现和 ANSI-SQL 标准之间的差异，在标准的传统实现中，RR 隔离级别是使用持续的 X 锁和持续的 S 锁来实现的（参看下面的 “隔离级别的实现” 一节），由于是持续的 S 锁，所以避免了其他事务有写操作，也就不存在提交覆盖问题。但是 MySQL 在 RR 隔离级别下，普通的 SELECT 语句只是快照读，没有任何的加锁，和标准的 RR 是不一样的。如果要让 MySQL 在 RR 隔离级别下不发生提交覆盖，可以使用 SELECT … LOCK IN SHARE MODE 或者 SELECT … FOR UPDATE 。</p>
<h1 id="常见锁类型"><a href="#常见锁类型" class="headerlink" title="常见锁类型"></a>常见锁类型</h1><h2 id="表锁-amp-行锁"><a href="#表锁-amp-行锁" class="headerlink" title="表锁 &amp; 行锁"></a>表锁 &amp; 行锁</h2><p>在 MySQL 中锁的种类有很多，但是最基本的还是表锁和行锁：表锁指的是对一整张表加锁，一般是 DDL 处理时使用，也可以自己在 SQL 中指定；而行锁指的是锁定某一行数据或某几行，或行和行之间的间隙。行锁的加锁方法比较复杂，但是由于只锁住有限的数据，对于其它数据不加限制，所以并发能力强，通常都是用行锁来处理并发事务。表锁由 MySQL 服务器实现，行锁由存储引擎实现，常见的就是 InnoDb，所以通常我们在讨论行锁时，隐含的一层意义就是数据库的存储引擎为 InnoDb ，而 MyISAM 存储引擎只能使用表锁。</p>
<h3 id="表锁"><a href="#表锁" class="headerlink" title="表锁"></a>表锁</h3><p>表锁由 MySQL 服务器实现，所以无论你的存储引擎是什么，都可以使用。一般在执行 DDL 语句时，譬如 ALTER TABLE 就会对整个表进行加锁。在执行 SQL 语句时，也可以明确对某个表加锁，譬如下面的例子<br><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line"><span class="meta">mysql&gt;</span><span class="bash"> lock table products <span class="built_in">read</span>;</span></span><br><span class="line">Query OK, 0 rows affected (0.00 sec)</span><br><span class="line"></span><br><span class="line"><span class="meta">mysql&gt;</span><span class="bash"> select * from products <span class="built_in">where</span> id = 100;</span></span><br><span class="line"></span><br><span class="line"><span class="meta">mysql&gt;</span><span class="bash"> unlock tables;</span></span><br><span class="line">Query OK, 0 rows affected (0.00 sec)</span><br></pre></td></tr></table></figure><br>上面的 SQL 首先对 products 表加一个表锁，然后执行查询语句，最后释放表锁。<br>表锁可以细分成两种：读锁和写锁，如果是加写锁，则是 <code>lock table products write</code> 。</p>
<p>关于表锁，我们要了解它的加锁和解锁原则，要注意的是它使用的是 <strong>一次封锁</strong> 技术，也就是说，我们会在会话开始的地方使用 lock 命令将后面所有要用到的表加上锁，在锁释放之前，我们只能访问这些加锁的表，不能访问其他的表，最后通过 unlock tables 释放所有表锁。这样的好处是，不会发生死锁！所以我们在 MyISAM 存储引擎中，是不可能看到死锁场景的。对多个表加锁的例子如下：</p>
<figure class="highlight shell"><table><tr><td class="code"><pre><span class="line"><span class="meta">mysql&gt;</span><span class="bash"> lock table products <span class="built_in">read</span>, orders <span class="built_in">read</span>;</span></span><br><span class="line">Query OK, 0 rows affected (0.00 sec)</span><br><span class="line"></span><br><span class="line"><span class="meta">mysql&gt;</span><span class="bash"> select * from products <span class="built_in">where</span> id = 100;</span></span><br><span class="line"></span><br><span class="line"><span class="meta">mysql&gt;</span><span class="bash"> select * from orders <span class="built_in">where</span> id = 200;</span></span><br><span class="line"></span><br><span class="line"><span class="meta">mysql&gt;</span><span class="bash"> select * from users <span class="built_in">where</span> id = 300;</span></span><br><span class="line">ERROR 1100 (HY000): Table &#x27;users&#x27; was not locked with LOCK TABLES</span><br><span class="line"></span><br><span class="line"><span class="meta">mysql&gt;</span><span class="bash"> update orders <span class="built_in">set</span> price = 5000 <span class="built_in">where</span> id = 200;</span></span><br><span class="line">ERROR 1099 (HY000): Table &#x27;orders&#x27; was locked with a READ lock and can&#x27;t be updated</span><br><span class="line"></span><br><span class="line"><span class="meta">mysql&gt;</span><span class="bash"> unlock tables;</span></span><br><span class="line">Query OK, 0 rows affected (0.00 sec)</span><br></pre></td></tr></table></figure>
<p>可以看到由于没有对 users 表加锁，在持有表锁的情况下是不能读取的，另外，由于加的是读锁，所以后面也不能对 orders 表进行更新。MySQL 表锁的加锁规则如下：</p>
<ul>
<li>对于读锁<ul>
<li>持有读锁的会话可以读表，但不能写表；</li>
<li>允许多个会话同时持有读锁；</li>
<li>其他会话就算没有给表加读锁，也是可以读表的，但是不能写表；</li>
<li>其他会话申请该表写锁时会阻塞，直到锁释放。</li>
</ul>
</li>
<li>对于写锁<ul>
<li>持有写锁的会话既可以读表，也可以写表；</li>
<li>只有持有写锁的会话才可以访问该表，其他会话访问该表会被阻塞，直到锁释放；</li>
<li>其他会话无论申请该表的读锁或写锁，都会阻塞，直到锁释放。</li>
</ul>
</li>
</ul>
<p>锁的释放规则如下：</p>
<ul>
<li>使用 UNLOCK TABLES 语句可以显示释放表锁；</li>
<li>如果会话在持有表锁的情况下执行 LOCK TABLES 语句，将会释放该会话之前持有的锁；</li>
<li>如果会话在持有表锁的情况下执行 START TRANSACTION 或 BEGIN 开启一个事务，将会释放该会话之前持有的锁；</li>
<li>如果会话连接断开，将会释放该会话所有的锁。</li>
</ul>
<h3 id="行锁"><a href="#行锁" class="headerlink" title="行锁"></a>行锁</h3><p>表锁不仅实现和使用都很简单，而且占用的系统资源少，所以在很多存储引擎中使用，如 MyISAM、MEMORY、MERGE 等，MyISAM 存储引擎几乎完全依赖 MySQL 服务器提供的表锁机制，查询自动加表级读锁，更新自动加表级写锁，以此来解决可能的并发问题。但是表锁的粒度太粗，导致数据库的并发性能降低，为了提高数据库的并发能力，InnoDb 引入了行锁的概念。行锁和表锁对比如下：</p>
<ul>
<li>表锁：开销小，加锁快；不会出现死锁；锁定粒度大，发生锁冲突的概率最高，并发度最低；</li>
<li>行锁：开销大，加锁慢；会出现死锁；锁定粒度最小，发生锁冲突的概率最低，并发度也最高。</li>
</ul>
<p>行锁和表锁一样，也分成两种类型：读锁和写锁。常见的增删改（INSERT、DELETE、UPDATE）语句会自动对操作的数据行加写锁，查询的时候也可以明确指定锁的类型，SELECT … LOCK IN SHARE MODE 语句加的是读锁，SELECT … FOR UPDATE 语句加的是写锁。</p>
<p>行锁这个名字听起来像是这个锁加在某个数据行上，实际上这里要指出的是：在 MySQL 中，行锁是加在索引上的。</p>
<h4 id="MySQL-加锁流程"><a href="#MySQL-加锁流程" class="headerlink" title="MySQL 加锁流程"></a>MySQL 加锁流程</h4><p>当执行下面的 SQL 时（id 为 students 表的主键），我们要知道，InnoDb 存储引擎会在 id = 49 这个主键索引上加一把 X 锁。<br><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line"><span class="meta">mysql&gt;</span><span class="bash"> update students <span class="built_in">set</span> score = 100 <span class="built_in">where</span> id = 49;</span></span><br></pre></td></tr></table></figure><br>当执行下面的 SQL 时（name 为 students 表的二级索引），InnoDb 存储引擎会在 name = ‘Tom’ 这个索引上加一把 X 锁，同时会通过 name = ‘Tom’ 这个二级索引定位到 id = 49 这个主键索引，并在 id = 49 这个主键索引上加一把 X 锁。<br><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line"><span class="meta">mysql&gt;</span><span class="bash"> update students <span class="built_in">set</span> score = 100 <span class="built_in">where</span> name = <span class="string">&#x27;Tom&#x27;</span>;</span></span><br></pre></td></tr></table></figure><br>加锁过程如下图所示：<br><img src="https://image-hosting-1251780645.cos.ap-beijing.myqcloud.com/fromSina/006tNbRwgy1fvg7zp5m7uj30ir0f7jsk.jpg" alt=""></p>
<p>多条记录的加锁流程：<br><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line"><span class="meta">mysql&gt;</span><span class="bash"> update students <span class="built_in">set</span> level = 3 <span class="built_in">where</span> score &gt;= 60;</span></span><br></pre></td></tr></table></figure><br>下图展示了当用户执行这条 SQL 时，MySQL Server 和 InnoDb 之间的执行流程：<br><img src="https://image-hosting-1251780645.cos.ap-beijing.myqcloud.com/fromSina/006tNbRwgy1fvg7yyf7rhj30i50dp0tl.jpg" alt=""><br>MySQL 在操作多条记录时 InnoDB 与 MySQL Server 的交互是一条一条进行的，加锁也是一条一条依次进行的，先对一条满足条件的记录加锁，返回给 MySQL Server，做一些 DML 操作，然后在读取下一条加锁，直至读取完毕。</p>
<h4 id="行锁种类"><a href="#行锁种类" class="headerlink" title="行锁种类"></a>行锁种类</h4><p>根据锁的粒度可以把锁细分为表锁和行锁，行锁根据场景的不同又可以进一步细分，在 MySQL 的源码里，定义了四种类型的行锁，如下：<br><figure class="highlight c"><table><tr><td class="code"><pre><span class="line"><span class="meta">#<span class="meta-keyword">define</span> LOCK_TABLE  16  <span class="comment">/* table lock */</span></span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">define</span> LOCK_REC    32  <span class="comment">/* record lock */</span></span></span><br><span class="line"></span><br><span class="line"><span class="comment">/* Precise modes */</span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">define</span> LOCK_ORDINARY   0   </span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">define</span> LOCK_GAP    512</span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">define</span> LOCK_REC_NOT_GAP 1024   </span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">define</span> LOCK_INSERT_INTENTION 2048</span></span><br></pre></td></tr></table></figure></p>
<ul>
<li>LOCK_ORDINARY：也称为 Next-Key Lock，锁一条记录及其之前的间隙，这是 RR 隔离级别用的最多的锁，从名字也能看出来；</li>
<li>LOCK_GAP：间隙锁，锁两个记录之间的 GAP，防止记录插入；</li>
<li>LOCK_REC_NOT_GAP：只锁记录；</li>
<li>LOCK_INSERT_INTENSION：插入意向 GAP 锁，插入记录时使用，是 LOCK_GAP 的一种特例。</li>
</ul>
<h2 id="读锁-amp-写锁"><a href="#读锁-amp-写锁" class="headerlink" title="读锁 &amp; 写锁"></a>读锁 &amp; 写锁</h2><p>MySQL 将锁分成两类：锁类型（lock_type）和锁模式（lock_mode）。锁类型就是上文中介绍的表锁和行锁两种类型，当然行锁还可以细分成记录锁和间隙锁等更细的类型，锁类型描述的锁的粒度，也可以说是把锁具体加在什么地方；而锁模式描述的是到底加的是什么锁，譬如读锁或写锁。锁模式通常是和锁类型结合使用的，锁模式在 MySQL 的源码中定义如下：<br><figure class="highlight cpp"><table><tr><td class="code"><pre><span class="line"><span class="comment">/* Basic lock modes */</span></span><br><span class="line"><span class="class"><span class="keyword">enum</span> <span class="title">lock_mode</span> &#123;</span></span><br><span class="line">    LOCK_IS = <span class="number">0</span>, <span class="comment">/* intention shared */</span></span><br><span class="line">    LOCK_IX,    <span class="comment">/* intention exclusive */</span></span><br><span class="line">    LOCK_S,     <span class="comment">/* shared */</span></span><br><span class="line">    LOCK_X,     <span class="comment">/* exclusive */</span></span><br><span class="line">    LOCK_AUTO_INC,  <span class="comment">/* locks the auto-inc counter of a table in an exclusive mode*/</span></span><br><span class="line">    ...</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure></p>
<ul>
<li>LOCK_IS：读意向锁；</li>
<li>LOCK_IX：写意向锁；</li>
<li>LOCK_S：读锁；</li>
<li>LOCK_X：写锁；</li>
<li>LOCK_AUTO_INC：自增锁；</li>
</ul>
<p>将锁分为读锁和写锁主要是为了提高读的并发，如果不区分读写锁，那么数据库将没办法并发读，并发性将大大降低。而 IS（读意向）、IX（写意向）只会应用在表锁上，方便表锁和行锁之间的冲突检测。LOCK_AUTO_INC 是一种特殊的表锁。下面依次进行介绍。</p>
<h3 id="读写锁"><a href="#读写锁" class="headerlink" title="读写锁"></a>读写锁</h3><p>读锁和写锁都是最基本的锁模式，它们的概念也比较容易理解。读锁，又称共享锁（Share locks，简称 S 锁），加了读锁的记录，所有的事务都可以读取，但是不能修改，并且可同时有多个事务对记录加读锁。写锁，又称排他锁（Exclusive locks，简称 X 锁），或独占锁，对记录加了排他锁之后，只有拥有该锁的事务可以读取和修改，其他事务都不可以读取和修改，并且同一时间只能有一个事务加写锁。（注意：这里说的读都是当前读，快照读是无需加锁的，记录上无论有没有锁，都可以快照读）</p>
<h3 id="读写意向锁"><a href="#读写意向锁" class="headerlink" title="读写意向锁"></a>读写意向锁</h3><p>表锁锁定了整张表，而行锁是锁定表中的某条记录，它们俩锁定的范围有交集，因此表锁和行锁之间是有冲突的。譬如某个表有 10000 条记录，其中有一条记录加了 X 锁，如果这个时候系统需要对该表加表锁，为了判断是否能加这个表锁，系统需要遍历表中的所有 10000 条记录，看看是不是某条记录被加锁，如果有锁，则不允许加表锁，显然这是很低效的一种方法，为了方便检测表锁和行锁的冲突，从而引入了意向锁。</p>
<p>意向锁为表级锁，也可分为读意向锁（IS 锁）和写意向锁（IX 锁）。当事务试图读或写某一条记录时，会先在表上加上意向锁，然后才在要操作的记录上加上读锁或写锁。这样判断表中是否有记录加锁就很简单了，只要看下表上是否有意向锁就行了。意向锁之间是不会产生冲突的，也不和 AUTO_INC 表锁冲突，它只会阻塞表级读锁或表级写锁，另外，意向锁也不会和行锁冲突，行锁只会和行锁冲突。</p>
<p>下面是各个表锁之间的兼容矩阵：<br><img src="https://image-hosting-1251780645.cos.ap-beijing.myqcloud.com/fromSina/006tNbRwgy1fvg896mbu6j30l206q3z4.jpg" alt=""></p>
<h3 id="AUTO-INC-锁"><a href="#AUTO-INC-锁" class="headerlink" title="AUTO_INC 锁"></a>AUTO_INC 锁</h3><p>AUTO_INC 锁又叫自增锁（一般简写成 AI 锁），它是一种特殊类型的表锁，当插入的表中有自增列（AUTO_INCREMENT）的时候可能会遇到。当插入表中有自增列时，数据库需要自动生成自增值，在生成之前，它会先为该表加 AUTO_INC 表锁，其他事务的插入操作阻塞，这样保证生成的自增值肯定是唯一的。AUTO_INC 锁具有如下特点：</p>
<ul>
<li>AUTO_INC 锁互不兼容，也就是说同一张表同时只允许有一个自增锁；</li>
<li>自增锁不遵循二段锁协议，它并不是事务结束时释放，而是在 INSERT 语句执行结束时释放，这样可以提高并发插入的性能。</li>
<li>自增值一旦分配了就会 +1，如果事务回滚，自增值也不会减回去，所以自增值可能会出现中断的情况。</li>
</ul>
<p>显然，AUTO_INC 表锁会导致并发插入的效率降低，为了提高插入的并发性，MySQL 从 5.1.22 版本开始，引入了一种可选的轻量级锁（mutex）机制来代替 AUTO_INC 锁，我们可以通过参数 innodb_autoinc_lock_mode 控制分配自增值时的并发策略。参数 innodb_autoinc_lock_mode 可以取下列值：</p>
<ul>
<li><p>innodb_autoinc_lock_mode = 0 （traditional lock mode）</p>
<ul>
<li>使用传统的 AUTO_INC 表锁，并发性比较差。</li>
</ul>
</li>
<li><p>innodb_autoinc_lock_mode = 1 （consecutive lock mode）</p>
<ul>
<li>MySQL 默认采用这种方式，是一种比较折中的方法。</li>
<li>MySQL 将插入语句分成三类：<code>Simple inserts</code>、<code>Bulk inserts</code>、<code>Mixed-mode inserts</code>。通过分析 INSERT 语句可以明确知道插入数量的叫做 <code>Simple inserts</code>，譬如最经常使用的 INSERT INTO table VALUE(1,2) 或 INSERT INTO table VALUES(1,2), (3,4)；通过分析 INSERT 语句无法确定插入数量的叫做 Bulk inserts，譬如 INSERT INTO table SELECT 或 LOAD DATA 等；还有一种是不确定是否需要分配自增值的，譬如 INSERT INTO table VALUES(1,’a’), (NULL,’b’), (5, ‘C’), (NULL, ‘d’) 或 INSERT … ON DUPLICATE KEY UPDATE，这种叫做 <code>Mixed-mode inserts</code>。</li>
<li>Bulk inserts 不能确定插入数使用表锁；Simple inserts 和 Mixed-mode inserts 使用轻量级锁 mutex，只锁住预分配自增值的过程，不锁整张表。Mixed-mode inserts 会直接分析语句，获得最坏情况下需要插入的数量，一次性分配足够的自增值，缺点是会分配过多，导致浪费和空洞。</li>
<li>这种模式的好处是既平衡了并发性，又能保证同一条 INSERT 语句分配的自增值是连续的。</li>
</ul>
</li>
<li><p>innodb_autoinc_lock_mode = 2 （interleaved lock mode）</p>
<ul>
<li>全部都用轻量级锁 mutex，并发性能最高，按顺序依次分配自增值，不会预分配。</li>
<li>缺点是不能保证同一条 INSERT 语句内的自增值是连续的，这样在复制（replication）时，如果 binlog_format 为 statement-based（基于语句的复制）就会存在问题，因为是来一个分配一个，同一条 INSERT 语句内获得的自增值可能不连续，主从数据集会出现数据不一致。所以在做数据库同步时要特别注意这个配置。</li>
</ul>
</li>
</ul>
<h2 id="细说行锁"><a href="#细说行锁" class="headerlink" title="细说行锁"></a>细说行锁</h2><p>前面在讲行锁时有提到，在 MySQL 的源码中定义了四种类型的行锁，我们这一节将学习这四种锁。</p>
<h3 id="记录锁-Record-Locks"><a href="#记录锁-Record-Locks" class="headerlink" title="记录锁 Record Locks"></a>记录锁 Record Locks</h3><p>记录锁 是最简单的行锁。<br><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line"><span class="meta">mysql&gt;</span><span class="bash"> UPDATE accounts SET level = 100 WHERE id = 5;</span></span><br></pre></td></tr></table></figure><br>这条 SQL 语句就会在 id = 5 这条记录上加上记录锁，防止其他事务对 id = 5 这条记录进行修改或删除。记录锁永远都是加在索引上的，就算一个表没有建索引，数据库也会隐式的创建一个索引。如果 WHERE 条件中指定的列是个二级索引，那么记录锁不仅会加在这个二级索引上，还会加在这个二级索引所对应的聚簇索引上（参考上面的加锁流程一节）。</p>
<p>注意，如果 SQL 语句无法使用索引时会走主索引实现全表扫描，这个时候 MySQL 会给整张表的所有数据行加记录锁。如果一个 WHERE 条件无法通过索引快速过滤，存储引擎层面就会将所有记录加锁后返回，再由 MySQL Server 层进行过滤。不过在实际使用过程中，MySQL 做了一些改进，在 MySQL Server 层进行过滤的时候，如果发现不满足，会调用 unlock_row 方法，把不满足条件的记录释放锁（显然这违背了两段锁协议）。这样做，保证了最后只会持有满足条件记录上的锁，但是每条记录的加锁操作还是不能省略的。可见在没有索引时，不仅会消耗大量的锁资源，增加数据库的开销，而且极大的降低了数据库的并发性能，所以说，更新操作一定要记得走索引。</p>
<h3 id="间隙锁-Gap-Locks"><a href="#间隙锁-Gap-Locks" class="headerlink" title="间隙锁 Gap Locks"></a>间隙锁 Gap Locks</h3><p>还是看上面的那个例子，如果 id = 5 这条记录不存在，这个 SQL 语句还会加锁吗？<br>这个 SQL 语句在 RC 隔离级别不会加任何锁，在 RR 隔离级别为了避免幻读会在 id = 5 前后两个索引之间加上间隙锁。<br>间隙锁和间隙锁之间是互不冲突的，间隙锁唯一的作用就是为了防止其他事务的插入。</p>
<h3 id="Next-Key-Locks"><a href="#Next-Key-Locks" class="headerlink" title="Next-Key Locks"></a>Next-Key Locks</h3><p>Next-key 锁 是记录锁和间隙锁的组合，它指的是加在某条记录以及这条记录前面间隙上的锁。假设一个索引包含10、11、13 和 20 这几个值，可能的 Next-key 锁如下：</p>
<figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">(-∞, 10]</span><br><span class="line">(10, 11]</span><br><span class="line">(11, 13]</span><br><span class="line">(13, 20]</span><br><span class="line">(20, +∞)</span><br></pre></td></tr></table></figure>
<p>通常我们都用这种左开右闭区间来表示 Next-key 锁，前面四个都是 Next-key 锁，最后一个为间隙锁。<br>和间隙锁一样，在 RC 隔离级别下没有 Next-key 锁，只有 RR 隔离级别才有。继续拿上面的 SQL 例子来说，如果 id 不是主键，而是二级索引，且不是唯一索引，那么这个 SQL 在 RR 隔离级别下会加什么锁呢？答案就是 Next-key 锁，如下：<br><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">(a, 5]</span><br><span class="line">(5, b)</span><br></pre></td></tr></table></figure><br>其中，a 和 b 是 id = 5 前后两个索引，我们假设 a = 1、b = 10，那么此时如果插入一条 id = 3 的记录将会阻塞住。之所以要把 id = 5 前后的间隙都锁住，仍然是为了解决幻读问题，因为 id 是非唯一索引，所以 id = 5 可能会有多条记录，为了防止再插入一条 id = 5 的记录，必须将下面标记 <code>^</code> 的位置都锁住，因为这些位置都可能再插入一条 id = 5 的记录：<br><code>1 ^ 5 ^ 5 ^ 5 ^ 10 11 13 15</code><br>可以看出来，Next-key 锁确实可以避免幻读，但是带来的副作用是连插入 id = 3 这样的记录也被阻塞了。</p>
<p>关于 Next-key 锁，有一个比较有意思的问题，比如下面这个 orders 表（id 为主键，order_id 为二级非唯一索引）：</p>
<figure class="highlight shell"><table><tr><td class="code"><pre><span class="line">+-----+----------+</span><br><span class="line">|  id | order_id |</span><br><span class="line">+-----+----------+</span><br><span class="line">|   1 |        1 |</span><br><span class="line">|   3 |        2 |</span><br><span class="line">|   5 |        5 |</span><br><span class="line">|   7 |        5 |</span><br><span class="line">|  10 |        9 |</span><br><span class="line">+-----+----------+</span><br></pre></td></tr></table></figure>
<p>这个时候不仅 order_id = 5 这条记录会加上 X 记录锁，而且这条记录前后的间隙也会加上锁，加锁位置如下：<br><code>1 2 ^ 5 ^ 5 ^ 9</code><br>可以看到 (2, 9) 这个区间都被锁住了，这个时候如果插入 order_id = 4 或者 order_id = 8 这样的记录肯定会被阻塞，这没什么问题，那么现在问题来了，如果插入一条记录 order_id = 2 或者 order_id = 9 会被阻塞吗？答案是可能阻塞，也可能不阻塞，这取决于插入记录主键的值，感兴趣的可以参考<a target="_blank" rel="noopener" href="http://blog.sina.com.cn/s/blog_a1e9c7910102vnrj.html">这篇博客</a>。</p>
<h3 id="插入意向锁-Insert-Intention-Locks"><a href="#插入意向锁-Insert-Intention-Locks" class="headerlink" title="插入意向锁 Insert Intention Locks"></a>插入意向锁 Insert Intention Locks</h3><p><a target="_blank" rel="noopener" href="https://dev.mysql.com/doc/refman/5.7/en/innodb-locking.html#innodb-insert-intention-locks">插入意向锁</a> 是一种特殊的间隙锁（所以有的地方把它简写成 II GAP），这个锁表示插入的意向，只有在 INSERT 的时候才会有这个锁。注意，这个锁虽然也叫意向锁，但是和上面介绍的表级意向锁是两个完全不同的概念，不要搞混淆了。插入意向锁和插入意向锁之间互不冲突，所以可以在同一个间隙中有多个事务同时插入不同索引的记录。譬如在上面的例子中，id = 1 和 id = 5 之间如果有两个事务要同时分别插入 id = 2 和 id = 3 是没问题的，虽然两个事务都会在 id = 1 和 id = 5 之间加上插入意向锁，但是不会冲突。</p>
<p>插入意向锁只会和间隙锁或 Next-key 锁冲突，正如上面所说，间隙锁唯一的作用就是防止其他事务插入记录造成幻读，那么间隙锁是如何防止幻读的呢？正是由于在执行 INSERT 语句时需要加插入意向锁，而插入意向锁和间隙锁冲突，从而阻止了插入操作的执行。</p>
<h3 id="行锁的兼容矩阵"><a href="#行锁的兼容矩阵" class="headerlink" title="行锁的兼容矩阵"></a>行锁的兼容矩阵</h3><p>下面我们对这四种行锁做一个总结，它们之间的兼容矩阵如下图所示：<br><img src="https://image-hosting-1251780645.cos.ap-beijing.myqcloud.com/fromSina/006tNbRwgy1fvg952ozxrj30oo05r3z8.jpg" alt=""><br>其中，<strong>行表示已有的锁，列表示要加的锁。</strong></p>
<h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><ol>
<li><a target="_blank" rel="noopener" href="http://www.aneasystone.com/archives/2017/10/solving-dead-locks-one.html">学习事务与隔离级别</a></li>
<li><a target="_blank" rel="noopener" href="http://www.aneasystone.com/archives/2017/11/solving-dead-locks-two.html">了解常见锁类型</a></li>
<li><a target="_blank" rel="noopener" href="http://www.imooc.com/article/17290">MySQL数据库事务各隔离级别加锁情况—read committed &amp;&amp; MVCC</a></li>
</ol>

    </div>
    <br>
    <div>全文完。</div>
  </article>
  <div class="toc-container">
    
  <div id="toc" class="toc-article">
    <strong class="toc-title">目录</strong>
    <ol class="toc"><li class="toc-item toc-level-1"><a class="toc-link" href="#%E4%BA%8B%E5%8A%A1%E4%B8%8E%E9%9A%94%E7%A6%BB%E7%BA%A7%E5%88%AB"><span class="toc-text">事务与隔离级别</span></a><ol class="toc-child"><li class="toc-item toc-level-2"><a class="toc-link" href="#%E4%BA%8B%E5%8A%A1%E5%9B%9B%E8%A6%81%E7%B4%A0"><span class="toc-text">事务四要素</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E5%B9%B6%E5%8F%91%E9%97%AE%E9%A2%98%E5%90%8D%E8%AF%8D%E8%A7%A3%E9%87%8A"><span class="toc-text">并发问题名词解释</span></a><ol class="toc-child"><li class="toc-item toc-level-3"><a class="toc-link" href="#%E8%84%8F%E8%AF%BB-dirty-read"><span class="toc-text">脏读 dirty read</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E4%B8%8D%E5%8F%AF%E9%87%8D%E5%A4%8D%E8%AF%BB-unrepeatable-read"><span class="toc-text">不可重复读 unrepeatable read</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E5%B9%BB%E8%AF%BB-phantom-read"><span class="toc-text">幻读 phantom read</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E4%B8%A2%E5%A4%B1%E6%9B%B4%E6%96%B0-lost-update"><span class="toc-text">丢失更新 lost update</span></a></li></ol></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E9%9A%94%E7%A6%BB%E7%BA%A7%E5%88%AB"><span class="toc-text">隔离级别</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E9%9A%94%E7%A6%BB%E7%BA%A7%E5%88%AB%E7%9A%84%E5%AE%9E%E7%8E%B0"><span class="toc-text">隔离级别的实现</span></a><ol class="toc-child"><li class="toc-item toc-level-3"><a class="toc-link" href="#%E4%BC%A0%E7%BB%9F%E7%9A%84%E9%9A%94%E7%A6%BB%E7%BA%A7%E5%88%AB"><span class="toc-text">传统的隔离级别</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#MySQL%E7%9A%84%E9%9A%94%E7%A6%BB%E7%BA%A7%E5%88%AB"><span class="toc-text">MySQL的隔离级别</span></a></li></ol></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E7%AD%94%E6%A1%88"><span class="toc-text">答案</span></a></li></ol></li><li class="toc-item toc-level-1"><a class="toc-link" href="#%E5%B8%B8%E8%A7%81%E9%94%81%E7%B1%BB%E5%9E%8B"><span class="toc-text">常见锁类型</span></a><ol class="toc-child"><li class="toc-item toc-level-2"><a class="toc-link" href="#%E8%A1%A8%E9%94%81-amp-%E8%A1%8C%E9%94%81"><span class="toc-text">表锁 &amp; 行锁</span></a><ol class="toc-child"><li class="toc-item toc-level-3"><a class="toc-link" href="#%E8%A1%A8%E9%94%81"><span class="toc-text">表锁</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E8%A1%8C%E9%94%81"><span class="toc-text">行锁</span></a><ol class="toc-child"><li class="toc-item toc-level-4"><a class="toc-link" href="#MySQL-%E5%8A%A0%E9%94%81%E6%B5%81%E7%A8%8B"><span class="toc-text">MySQL 加锁流程</span></a></li><li class="toc-item toc-level-4"><a class="toc-link" href="#%E8%A1%8C%E9%94%81%E7%A7%8D%E7%B1%BB"><span class="toc-text">行锁种类</span></a></li></ol></li></ol></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E8%AF%BB%E9%94%81-amp-%E5%86%99%E9%94%81"><span class="toc-text">读锁 &amp; 写锁</span></a><ol class="toc-child"><li class="toc-item toc-level-3"><a class="toc-link" href="#%E8%AF%BB%E5%86%99%E9%94%81"><span class="toc-text">读写锁</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E8%AF%BB%E5%86%99%E6%84%8F%E5%90%91%E9%94%81"><span class="toc-text">读写意向锁</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#AUTO-INC-%E9%94%81"><span class="toc-text">AUTO_INC 锁</span></a></li></ol></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E7%BB%86%E8%AF%B4%E8%A1%8C%E9%94%81"><span class="toc-text">细说行锁</span></a><ol class="toc-child"><li class="toc-item toc-level-3"><a class="toc-link" href="#%E8%AE%B0%E5%BD%95%E9%94%81-Record-Locks"><span class="toc-text">记录锁 Record Locks</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E9%97%B4%E9%9A%99%E9%94%81-Gap-Locks"><span class="toc-text">间隙锁 Gap Locks</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#Next-Key-Locks"><span class="toc-text">Next-Key Locks</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E6%8F%92%E5%85%A5%E6%84%8F%E5%90%91%E9%94%81-Insert-Intention-Locks"><span class="toc-text">插入意向锁 Insert Intention Locks</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E8%A1%8C%E9%94%81%E7%9A%84%E5%85%BC%E5%AE%B9%E7%9F%A9%E9%98%B5"><span class="toc-text">行锁的兼容矩阵</span></a></li></ol></li></ol></li><li class="toc-item toc-level-1"><a class="toc-link" href="#%E5%8F%82%E8%80%83"><span class="toc-text">参考</span></a></li></ol>
  </div>


  </div>
</div>
<div class="copyright">
    <span>本作品采用</span>
    <a target="_blank" rel="noopener" href="https://creativecommons.org/licenses/by/4.0/">知识共享署名 4.0 国际许可协议</a>
    <span>进行许可。 转载时请注明原文链接。</span>
</div>
<div class="share" style="width: 100%;">
  <img src="/images/wechat-qcode.jpg" height="200" width="200" alt="" style="margin: auto; display: block;" />

  <div style="margin: auto; text-align: center; font-size: 0.8em; color: grey;">关注“豆菽技术”公众号</div>
  
</div>

  
    <div class="post-nav">
      <div class="post-nav-item post-nav-next">
        
          <span>〈 </span>
          <a href="/meaning-of-retained-size.html" rel="next" title="理解VisualVM中的保留大小(Retained Size)">
          理解VisualVM中的保留大小(Retained Size)
          </a>
        
      </div>
  
      <div class="post-nav-item post-nav-prev">
          
          <a href="/idempotence-design.html" rel="prev" title="幂等设计">
            幂等设计
          </a>
          <span>〉</span>
        
      </div>
    </div>
  

  <section class="disqus-comments">
    <div id="disqus_thread">
      <noscript>Please enable JavaScript to view the <a target="_blank" rel="noopener" href="//disqus.com/?ref_noscript">comments powered by Disqus.</a></noscript>
    </div>
  </section>

  <script>
    var disqus_shortname = 'coderbean';
    
    var disqus_url = 'https://jacobchang.cn/lock-of-mysql.html';
    
    (function(){
      var dsq = document.createElement('script');
      dsq.type = 'text/javascript';
      dsq.async = true;
      dsq.src = '//' + disqus_shortname + '.disqus.com/embed.js';
      (document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq);
    })();
  </script>

  <script id="dsq-count-scr" src="//coderbean.disqus.com/count.js" async></script>



    </div>

    

  </div>
  <footer class="footer text-center">
    <div id="bottom-inner">
        <a class="bottom-item-hide" href="https://jacobchang.cn/" target="_blank">主站</a>
        <!-- <a class="bottom-item-hide">·</a> -->
        <!-- <a class="bottom-item-hide" href="" target="_blank">备份站点</a> -->
        <a class="bottom-item-hide">·</a>
        <a class="bottom-item-hide" href="https://github.com/coderbean" target="_blank">GitHub</a>
        <a class="bottom-item-hide">·</a>
        <a class="bottom-item" href="https://hexo.io" target="_blank">Powered by hexo</a>
        <a class="bottom-item">·</a>
        <a class="bottom-item" href="https://github.com/coderbean/hexo-theme-xoxo" target="_blank">Theme xoxo</a>
    </div>
    <div id="bottom-inner">
        <a class="bottom-item-hide" target="_blank" rel="noopener" href="https://beian.miit.gov.cn">备案号：浙ICP备2021033778号-1</a> 
        <a class="bottom-item-hide">·</a>
        <a id="copyright" class="bottom-item"></a>
    </div>
</footer>

<script language="javascript" type="text/javascript">
    var df = new Date();
    var year = df.getFullYear();
    document.getElementById("copyright").innerHTML = "Copyright © 2017 – " + year + " 黄小豆";
</script>
  

<script>
  (function(window, document, undefined) {

    var timer = null;

    function returnTop() {
      cancelAnimationFrame(timer);
      timer = requestAnimationFrame(function fn() {
        var oTop = document.body.scrollTop || document.documentElement.scrollTop;
        if (oTop > 0) {
          document.body.scrollTop = document.documentElement.scrollTop = oTop - 50;
          timer = requestAnimationFrame(fn);
        } else {
          cancelAnimationFrame(timer);
        }
      });
    }

    var hearts = [];
    window.requestAnimationFrame = (function() {
      return window.requestAnimationFrame ||
        window.webkitRequestAnimationFrame ||
        window.mozRequestAnimationFrame ||
        window.oRequestAnimationFrame ||
        window.msRequestAnimationFrame ||
        function(callback) {
          setTimeout(callback, 1000 / 60);
        }
    })();
    init();

    function init() {
      css(".heart{z-index:9999;width: 10px;height: 10px;position: fixed;background: #f00;transform: rotate(45deg);-webkit-transform: rotate(45deg);-moz-transform: rotate(45deg);}.heart:after,.heart:before{content: '';width: inherit;height: inherit;background: inherit;border-radius: 50%;-webkit-border-radius: 50%;-moz-border-radius: 50%;position: absolute;}.heart:after{top: -5px;}.heart:before{left: -5px;}");
      attachEvent();
      gameloop();
      addMenuEvent();
    }

    function gameloop() {
      for (var i = 0; i < hearts.length; i++) {
        if (hearts[i].alpha <= 0) {
          document.body.removeChild(hearts[i].el);
          hearts.splice(i, 1);
          continue;
        }
        hearts[i].y--;
        hearts[i].scale += 0.004;
        hearts[i].alpha -= 0.013;
        hearts[i].el.style.cssText = "left:" + hearts[i].x + "px;top:" + hearts[i].y + "px;opacity:" + hearts[i].alpha + ";transform:scale(" + hearts[i].scale + "," + hearts[i].scale + ") rotate(45deg);background:" + hearts[i].color;
      }
      requestAnimationFrame(gameloop);
    }

    /**
     * 设置点击事件
     * 
     * - 回到顶部
     * - 出现爱心
     */
    function attachEvent() {
      var old = typeof window.onclick === "function" && window.onclick;
      var logo = document.getElementById("menu");
      if (logo) {
        logo.onclick = function(event) {
          // returnTop();
          old && old();
          createHeart(event);
        }
      }
      
    }

    function createHeart(event) {
      var d = document.createElement("div");
      d.className = "heart";
      hearts.push({
        el: d,
        x: event.clientX - 5,
        y: event.clientY - 5,
        scale: 1,
        alpha: 1,
        color: randomColor()
      });
      document.body.appendChild(d);
    }

    function css(css) {
      var style = document.createElement("style");
      style.type = "text/css";
      try {
        style.appendChild(document.createTextNode(css));
      } catch (ex) {
        style.styleSheet.cssText = css;
      }
      document.getElementsByTagName('head')[0].appendChild(style);
    }

    function randomColor() {
      // return "rgb(" + (~~(Math.random() * 255)) + "," + (~~(Math.random() * 255)) + "," + (~~(Math.random() * 255)) + ")";
      return "#F44336";
    }

    function addMenuEvent() {
      var menu = document.getElementById('menu-main-post');
      if (menu) {
        var toc = document.getElementById('toc');
        if (toc) {
          menu.onclick = function() {
            if (toc) {
              if (toc.style.display == 'block') {
                toc.style.display = 'none';
              } else {
                toc.style.display = 'block';
              }
            }
          };
        } else {
          menu.style.display = 'none';
        }
      }
    }

  })(window, document);
</script>

  


  <script>
      (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
                  (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
              m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
      })(window,document,'script','//www.google-analytics.com/analytics.js','ga');
      ga('create', 'UA-155080039-1', 'auto');
      ga('send', 'pageview');
  </script>


  
<link rel='stylesheet' href='https://s3-us-west-2.amazonaws.com/s.cdpn.io/1462889/unicons.css'>

<div class="progress-wrap">
    <svg class="progress-circle svg-content" width="100%" height="100%" viewBox="-1 -1 102 102">
        <path d="M50,1 a49,49 0 0,1 0,98 a49,49 0 0,1 0,-98" />
    </svg>
</div>
<script src='https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.0/jquery.min.js'></script>
<script>

    (function ($) {
        "use strict";
        $(document).ready(function () {
            "use strict";

            //Scroll back to top

            var progressPath = document.querySelector('.progress-wrap path');
            var pathLength = progressPath.getTotalLength();
            progressPath.style.transition = progressPath.style.WebkitTransition = 'none';
            progressPath.style.strokeDasharray = pathLength + ' ' + pathLength;
            progressPath.style.strokeDashoffset = pathLength;
            progressPath.getBoundingClientRect();
            progressPath.style.transition = progressPath.style.WebkitTransition = 'stroke-dashoffset 10ms linear';
            var updateProgress = function () {
                var scroll = $(window).scrollTop();
                var height = $(document).height() - $(window).height();
                var progress = pathLength - (scroll * pathLength / height);
                progressPath.style.strokeDashoffset = progress;
            }
            updateProgress();
            $(window).scroll(updateProgress);
            var offset = 50;
            var duration = 550;
            jQuery(window).on('scroll', function () {
                if (jQuery(this).scrollTop() > offset) {
                    jQuery('.progress-wrap').addClass('active-progress');
                } else {
                    jQuery('.progress-wrap').removeClass('active-progress');
                }
            });
            jQuery('.progress-wrap').on('click', function (event) {
                event.preventDefault();
                jQuery('html, body').animate({ scrollTop: 0 }, duration);
                return false;
            })


        });

    })(jQuery);
</script>

  
<!-- for theme: default is false -->
<!-- for page: default is true -->
<!-- 图片查看器实例配置 -->
<script type="text/javascript">
    //默认设置， 可以根据个人需求和喜好进行配置
    //详细参考官方说明
    Viewer.setDefaults({
        //设置初始缩放 default: 1
        zoomRatio: [0.5],
        // 过渡动画
        transition:true,
        // 显示工具条
        toolbar: false,
    });
    //获得content中所有的图片， 不同主题可能有所不同
    //为了和其他的图片区别开来 所以在markdown中插入图片的时候使用独特的记号
    //为了一次性得到所有的图片我这里采用的是class = 'article-image'
    var article = document.querySelector('.post-content');
    if(article!=null) {
        var imageList = article.getElementsByTagName('img');
        //将获取到的HTMLCollections转化成Array
        var imageArray = new Array();
        Array.prototype.forEach.call(imageList, element => {
            imageArray.push(element);
        });
        //设置每个图片成为图片组
        Array.prototype.forEach.call(imageList, element => {
            var viewer1 = new Viewer(element);
            viewer1.images = imageArray;
            viewer1.length = imageArray.length;
        });
    }

    
</script>

  
  <! -- mathjax config similar to math.stackexchange -->

<script type="text/x-mathjax-config">
MathJax.Hub.Config({
tex2jax: {
          inlineMath: [ ['$','$'], ["\\(","\\)"]  ],
                processEscapes: true
                    
}
  
        });
</script>

<script type="text/x-mathjax-config">
MathJax.Hub.Config({
tex2jax: {
            skipTags: ['script', 'noscript', 'style', 'textarea', 'pre', 'code']
                  
}
    
        });
</script>

<script type="text/x-mathjax-config">
MathJax.Hub.Queue(function() {
            var all = MathJax.Hub.getAllJax(), i;
            for(i=0; i < all.length; i += 1) {
                            all[i].SourceElement().parentNode.className += ' has-jax';
                                    
            }
                
        });
</script>

<script src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-MML-AM_CHTML"></script>

</script>

  
</body>

</html>